package org.clockworkmages.games.anno1186;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.clockworkmages.games.anno1186.model.character.GameCharacter;
import org.clockworkmages.games.anno1186.model.character.Skill;
import org.clockworkmages.games.anno1186.model.character.StatConstants;
import org.clockworkmages.games.anno1186.model.effect.Condition;
import org.clockworkmages.games.anno1186.model.effect.ConditionStatus;
import org.clockworkmages.games.anno1186.model.effect.Effect;
import org.clockworkmages.games.anno1186.model.effect.Perk;
import org.clockworkmages.games.anno1186.model.item.Item;
import org.clockworkmages.games.anno1186.scripting.ScriptingService;

public class GameCharacterService {

	@Injected
	private GameDataService gameDataService;

	@Injected
	private GameStateService gameStateService;

	@Injected
	private ScriptingService scriptingService;

	public void restoreAll(GameCharacter character) {
		double maxHp = character.getDerivates().get(StatConstants.D_MAX_HEALTH);
		character.getBaseStats().put(StatConstants.HEALTH, maxHp);

		double maxStamina = character.getDerivates().get(
				StatConstants.D_MAX_STAMINA);
		character.getBaseStats().put(StatConstants.STAMINA, maxStamina);

		Double maxFate = character.getEffectiveStats().get(
				StatConstants.MAX_FATE);
		// enemies do not have fate points:
		if (maxFate != null) {
			character.getBaseStats().put(StatConstants.FATE, maxFate);
		}

		character.setStatsAreStale(true);
	}

	public void refreshStats(GameCharacter character) {

		long gameTime = gameStateService.getGameState().getTime();

		GameData gameData = gameDataService.getGameData();

		// collect the effects that are set for the character, and sort out
		// those that are inactive (whose Conditions are not met):
		List<Effect> effects = new ArrayList<Effect>();

		// add common effects that define how base stats (e.g. ENDURANCE) get
		// translated to derivates (e.g. MAX_HEALTH)
		effects.addAll(gameData.getBaseEffects());

		// add character-specific stats:
		for (String perkId : character.getPerks()) {
			Perk perk = gameDataService.getGameData().getPerks().get(perkId);
			effects.addAll(perk.getEffects());
		}
		for (Item equippedItem : character.getEquippedItems().values()) {
			effects.addAll(equippedItem.getEffects());
		}

		List<EffectLevelPair> pairs = new ArrayList<EffectLevelPair>();
		for (Effect effect : effects) {
			pairs.add(new EffectLevelPair(effect, null));
		}

		for (Entry<String, ConditionStatus> entry : character.getConditions()
				.entrySet()) {
			String conditionId = entry.getKey();
			ConditionStatus conditionStatus = entry.getValue();
			long validUntil = conditionStatus.getValidUntil();
			if (validUntil != -1 && validUntil < gameTime) {
				// condition is no longer active, remove it:
				character.getConditions().remove(conditionId);
			} else {
				Condition condition = gameData.getConditions().get(conditionId);
				int level = conditionStatus.getLevel();
				// some conditions permanently affect base statistics (e.g.
				// increase or damage health) and can only be applied at
				// specified intervals.
				long lastExecuted = conditionStatus.getLastExecuted();
				Long interval = condition.getInterval();
				if (interval == null || interval < gameTime - lastExecuted) {
					for (Effect effect : condition.getEffects()) {
						pairs.add(new EffectLevelPair(effect, level));
					}
					conditionStatus.setLastExecuted(gameTime);
				}

			}
		}

		for (Entry<String, Integer> entry : character.getSkills().entrySet()) {
			String skillId = entry.getKey();
			int skillLevel = entry.getValue();
			Skill skill = gameData.getSkills().get(skillId);
			for (Effect effect : skill.getEffects()) {
				pairs.add(new EffectLevelPair(effect, skillLevel));
			}
		}

		List<EffectLevelPair> activePairs = new ArrayList<EffectLevelPair>();
		for (EffectLevelPair pair : pairs) {
			String condition = pair.effect.getCondition();
			if (condition == null || scriptingService.conditionIsMet(condition)) {
				activePairs.add(pair);
			}
		}

		Collections.sort(pairs);

		// clean up the old data:
		// we couldn't do it before checking the Effects' Conditions, in case
		// some conditions were based on derivates, such as
		// "only apply the Berzerk perk if currentHealth<50% of maxHealth".
		character.getEffectiveStats().clear();
		character.getDerivates().clear();
		for (Entry<String, Double> baseStat : character.getBaseStats()
				.entrySet()) {
			character.getEffectiveStats().put(baseStat.getKey(),
					baseStat.getValue());
		}

		// execute the active Effects:
		for (EffectLevelPair pair : activePairs) {
			boolean characterIsEnemy = character.isEnemy();
			Map<String, Object> model = scriptingService
					.getDefaultModel(characterIsEnemy);
			if (pair.level != null) {
				model.put("level", pair.level);
			}
			scriptingService.execute(pair.effect.getEffect(), model);
		}
		character.setStatsAreStale(false);
	}

	private class EffectLevelPair implements Comparable<EffectLevelPair> {

		public Effect effect;

		public Integer level;

		public EffectLevelPair(Effect effect, Integer level) {
			super();
			this.effect = effect;
			this.level = level;
		}

		@Override
		public int compareTo(EffectLevelPair that) {
			// sort so that ffects get executd in the following order:
			// - increase modifiedStat by value
			// - increase modifiedStat by %
			// - increase derivate by value
			// - increase derivate by %

			int thisPrio = getPrio(this.effect.getEffect());
			int thatPrio = getPrio(that.effect.getEffect());

			return Integer.valueOf(thisPrio).compareTo(thatPrio);
		}

		private int getPrio(String effect) {
			if (effect.startsWith("modifyStatPerc")) {
				return 2;
			}
			if (effect.startsWith("modifyStat")) {
				return 1;
			}

			if (effect.startsWith("modifyDerivatePerc")) {
				return 4;
			}
			if (effect.startsWith("modifyDerivate")) {
				return 3;
			}
			return 5;
		}
	}

}
